package com.riguz.forks.framework.ioc;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Qualifier;
import javax.inject.Singleton;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class Helper {
    private static final Logger logger = LoggerFactory.getLogger(Helper.class);
    
    private Helper() {

    }

    public static Set<Method> getProviders(final Class<?> type) {
        Set<Method> providers = new HashSet<>();
        for (Method method : type.getMethods()) {
            if (method.isAnnotationPresent(Provides.class)) providers.add(method);
        }
        return providers;
    }

    public static Annotation getQualifier(Annotation[] annotations) {
        for (Annotation a : annotations)
            if (a.annotationType().isAnnotationPresent(Qualifier.class)) return a;
        return null;
    }
    public static Set<Field> getFields(Class<?> type){
        Class<?> current = type;
        Set<Field> fields = new HashSet<>();
        while (!current.equals(Object.class)) {
            for (Field field : current.getDeclaredFields()) {
                if (field.isAnnotationPresent(Inject.class)) {
                    field.setAccessible(true);
                    fields.add(field);
                }
            }
            current = current.getSuperclass();
        }
        return fields;
        
    }
    
    public static class InjectField{
        Field field;
        boolean isProvider = false;
        Target<?> target;
        public InjectField(Field field, boolean isProvider, Target<?> target) {
            super();
            this.field = field;
            this.isProvider = isProvider;
            this.target = target;
        }
        public Field getField() {
            return field;
        }
        public void setField(Field field) {
            this.field = field;
        }
        public boolean isProvider() {
            return isProvider;
        }
        public void setProvider(boolean isProvider) {
            this.isProvider = isProvider;
        }
        public Target<?> getTarget() {
            return target;
        }
        public void setTarget(Target<?> target) {
            this.target = target;
        }
    }
    
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public static List<InjectField> getFieldsToInject(Class<?> type){
        Set<Field> fields = getFields(type);
        List<InjectField> result = new ArrayList<>();
        int i = 0;
        for(Field field:fields){
            Class<?> provider = null;
            Target<?> target = null;
            boolean isProvider = false;
            if(field.getType().equals(Provider.class)){
                provider = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
                target =new Target(provider);
                isProvider = true;
            }
            else{
                Annotation qualifier = Helper.getQualifier(field.getAnnotations());
                target = Target.of(field.getType(), qualifier);
            }
            InjectField f = new InjectField(field, isProvider, target);
            result.add(f);
        }
        return result;
    }

    public static boolean isSingletonProvider(Method method) {
        if (method.getAnnotation(Singleton.class) != null) return true;
        if (method.getReturnType().getAnnotation(Singleton.class) != null) return true;
        return false;
    }

    public static Constructor<?> getInjectConstructor(Class<?> type){
        Constructor<?> injectConstructor = null;
        Constructor<?> defaultConstructor = null;
        for(Constructor<?> c:type.getDeclaredConstructors()){
            if(c.isAnnotationPresent(Inject.class)){
                if(injectConstructor != null)
                    throw new IllegalArgumentException("Multiple inject constructors found in " + type.getName());
                injectConstructor = c;
            }
            if(c.getParameterTypes().length == 0)
                defaultConstructor = c;
        }
        Constructor<?> targetConstructor = injectConstructor;
        if(injectConstructor == null)
            targetConstructor = defaultConstructor;
        if(targetConstructor == null)
            throw new IllegalArgumentException("No inject constructor or no-arg constructor found in " + type.getName());
        targetConstructor.setAccessible(true);
        return targetConstructor;
    }
    
    public static Set<Target<?>> append(final Set<Target<?>> chain, final Target<?> newTarget) {
        if (chain == null) return Collections.singleton(newTarget);
        Set<Target<?>> newChain = new LinkedHashSet<>();
        newChain.add(newTarget);
        return newChain;
    }
    
    public static String printChain(final Set<Target<?>> chain, Target<?> target){
        StringBuilder chainString = new StringBuilder();
        for (Target<?> key : chain) 
            chainString.append(key.toString()).append(" -> ");
        return chainString.append(target.toString()).toString();
    }
    
    public static Object[] getParams(Provider<?>[] paramProviders){
        Object[] params = new Object[paramProviders.length];
        for (int i = 0; i < paramProviders.length; ++i)
            params[i] = paramProviders[i].get();
        return params;
    }
    
    public static <T> T createInstance(final Target<T> target, Constructor<T> constructor, final Provider<?>[] paramProviders){
        // create new singleton instance
        try {
            T instance =  constructor.newInstance(Helper.getParams(paramProviders));
            return instance;
        } catch (Exception e) {
            logger.error("Unable to instance object:{}", e);
        }
        return null;
    }
    
    @SuppressWarnings("unchecked")
    public static <T> T createInstance(final Target<T> target, Object module, Method method, final Provider<?>[] paramProviders){
        // create new singleton instance
        try {
            T instance = (T) method.invoke(module, Helper.getParams(paramProviders));
            return instance;
        } catch (Exception e) {
            logger.error("Unable to instance object:{}", e);
        }
        return null;
    }
}
